package com.ibm.cps.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Throwables;
import com.ibm.cps.message.MessageFields;
import com.ibm.cps.message.validation.ValidatorUtil;
import com.ibm.cps.newmessage.AbstractMetadata;
import com.ibm.cps.newmessage.DataSourceMetadataFactory;
import com.ibm.cps.newmessage.MetadataFactory;
import com.ibm.cps.processors.ProcessorFactory;
import com.ibm.cps.processors.ProcessorsGroup;
import com.ibm.factories.DataSourceFactory;
import com.ibm.factories.PublishProcessorFactory;
import com.ibm.util.ErrorCode;
import com.ibm.util.JSONUtility;
import com.ibm.util.RandomUtil;
import com.ibm.util.exception.CPSException;

public class NodeRedJsonAdapter extends MetadataAdapter {
	private String wireField = "wires";
	private String idField = "id";
	private String sourceGroupId = "z";
	private String targetGroupId = "groupid";
	HashMap<String, Set<String>> id2ParentProcessors = new HashMap<String, Set<String>>();
	private static final String DEFAULT_GROUPTID = "000000000000000000000000";
	private static Logger logger = LoggerFactory.getLogger(NodeRedJsonAdapter.class);

	public NodeRedJsonAdapter(DataSourceFactory factory, PublishProcessorFactory publishFactory) {
		super(factory, publishFactory);
	}

	/**
	 * convert following node red fields to the corresponding fields: id -->
	 * processorsid/groupid/schemaid, z ---> groupid, add parent processors to
	 * processor metadata according to wires, remove x, y, z, wires
	 * 
	 * @param nodeRedInput
	 * @param tenantid
	 * @return
	 * @throws CPSException
	 */
	@Override
	public String adapt2Json(String nodeRedInput, String tenantid) throws CPSException {
		try {
			ArrayNode array = JSONUtility.getArrayFromString(nodeRedInput);
			if (array == null || array.size() < 1) {
				return null;
			}

			String type;
			id2ParentProcessors = new HashMap<String, Set<String>>();
			constructParent2Successors(array);

			// adapte the node red format to formal json input
			for (int i = 0; i < array.size(); i++) {
				ObjectNode node = (ObjectNode) array.get(i);
				type = node.get(MessageFields.TYPE).asText();
				if (!ProcessorsGroup.isGroupType(type)) {
					addParentProcessor(node);
					replaceField(node, sourceGroupId, targetGroupId);
				}
				node.remove("x");
				node.remove("y");
				replaceIdField(node);
			}
			// processDefaultGroupid(array, tenantid);
			return array.toString();
		} catch (Exception e) {
			if (e instanceof CPSException) {
				throw e;
			} else {
				logger.error(Throwables.getStackTraceAsString(e));
				throw new CPSException(ErrorCode.INTERNAL_ERROR_CODE, "Error node-red format");
			}
		}
	}

	/**
	 * if the groupid is defaultid, then replace it to tenantid to prevent
	 * different tenants have the same default groupid
	 * 
	 * @param array
	 * @throws CPSException
	 */
	private void processDefaultGroupid(ArrayNode array, String tenantid) throws CPSException {
		ObjectNode objectNode;
		String groupid, type;
		boolean hasGroupTab = false;
		for (int i = 0; i < array.size(); i++) {
			objectNode = (ObjectNode) array.get(i);
			groupid = JSONUtility.GetString(MessageFields.GROUPID, objectNode);
			if (groupid.equals(DEFAULT_GROUPTID)) {
				objectNode.put(MessageFields.GROUPID, tenantid);
			}
			type = JSONUtility.GetString(MessageFields.TYPE, objectNode);
			if (ProcessorsGroup.isGroupType(type)) {
				hasGroupTab = true;
			}
		}

		if (!hasGroupTab) {
			ObjectNode group = genenrateDefaultGroup(tenantid);
			replaceIdField(group);
			array.add(group);
		}
	}

	/**
	 * the format of wires is: "wires": [ [ "000000000034215fbf0cbdea" ] ]
	 * convert all sucessors information to <parent, successors> map
	 * 
	 * @param array
	 */
	private void constructParent2Successors(ArrayNode array) {
		// get parent processors of all processors
		for (int i = 0; i < array.size(); i++) {
			JsonNode node = array.get(i);
			String processorid = node.get(idField).asText();
			if (node.get(wireField) != null) {
				ArrayNode wires = (ArrayNode) node.get(wireField);
				if (wires.get(0) != null) {
					ArrayNode successors = (ArrayNode) wires.get(0);
					for (int j = 0; j < successors.size(); j++) {
						String child = successors.get(j).asText();
						if (id2ParentProcessors.get(child) == null) {
							id2ParentProcessors.put(child, new HashSet<String>());
						}
						id2ParentProcessors.get(child).add(processorid);
					}
				}
			}
		}
	}

	private void addParentProcessor(ObjectNode node) {
		node.remove(wireField);
		String id = node.get(idField).asText();
		if (id2ParentProcessors.get(id) != null) {
			Set<String> parents = id2ParentProcessors.get(id);
			if (parents.size() == 1) {
				String parentid = generateId(parents.iterator().next());
				node.put(MessageFields.PARENTPROCESSORS, parentid);
			} else {
				ArrayNode array = JSONUtility.newArrayNode();
				for (String parent : parents) {
					array.add(generateId(parent));
				}
				node.set(MessageFields.PARENTPROCESSORS, array);
			}
		}
	}

	private void setSuccessors(ObjectNode node, HashMap<String, Set<String>> parent2Childrean) {
		node.remove(MessageFields.PARENTPROCESSORS);
		String id = node.get(idField).asText();
		ArrayNode wiresArray = JSONUtility.newArrayNode();
		ArrayNode childArray = JSONUtility.newArrayNode();
		if (parent2Childrean.get(id) != null) {
			for (String child : parent2Childrean.get(id)) {
				childArray.add(child);
			}
		}
		wiresArray.add(childArray);
		node.set(wireField, wiresArray);
	}

	private void replaceField(ObjectNode node, String sourceField, String targetField) throws CPSException {
		if (node.get(sourceField) == null) {
			throw new CPSException(ErrorCode.NULL_FIELD_ERROR, sourceField + " doesn't exist.");
		}
		String groupid = node.get(sourceField).asText();
		node.remove(sourceField);
		node.put(targetField, generateId(groupid));
	}

	private void replaceIdField(ObjectNode node) throws CPSException {
		if (node.get(idField) == null) {
			throw new CPSException(ErrorCode.NULL_FIELD_ERROR, idField + " doesn't exist.");
		}
		String id = node.get(idField).asText();
		node.remove(idField);
		String type = node.get(MessageFields.TYPE).asText();
		if (type.equals(MessageFields.SCHEMA_TYPE)) {
			node.put(MessageFields.SCHEMAID, generateId(id));
		} else if (type.equals(MessageFields.GROUP_TYPE_TAB)) {
			node.put(MessageFields.GROUPID, generateId(id));
		} else {
			node.put(MessageFields.PROCESSORID, generateId(id));
		}
	}

	private void resetIdField(ObjectNode node) throws CPSException {
		String type = node.get(MessageFields.TYPE).asText();
		if (ProcessorsGroup.isGroupType(type)) {
			node.put(idField, node.get(MessageFields.GROUPID).asText());
			node.remove(MessageFields.GROUPID);
		} else if (DataSourceMetadataFactory.isDataSourceType(type)) {
			node.put(idField, node.get(MessageFields.NAME).asText());
			node.remove(MessageFields.NAME);
		} else if (PublishProcessorFactory.isPublishProcessor(type)) {
			node.put(idField, node.get(MessageFields.NAME).asText());
			node.remove(MessageFields.NAME);
		} else if (ProcessorFactory.isProcessorType(type)) {
			node.put(sourceGroupId, node.get(MessageFields.GROUPID).asText());
			node.remove(MessageFields.GROUPID);
			if (node.get(sourceGroupId).asText().equals(ProcessorsGroup.DEFAULT_GROUPTID)) {
				node.put(sourceGroupId, 0);
			}
			node.put(idField, node.get(MessageFields.PROCESSORID).asText());
			node.remove(MessageFields.PROCESSORID);
		} else {
			throw new CPSException(ErrorCode.INVALID_INPUT_ERROR, "Unknown metadata type " + type + " in " + node);
		}
	}

	private String generateId(String id) {
		String newid = id.replace(".", "0");
		while (newid.length() < 24)
			newid = "0" + newid;
		return newid;
	}

	@Override
	public Collection<String> adaptFromJson(Collection<String> allMetadatas, Collection<String> targetMetadatas)
			throws CPSException {
		HashMap<String, Set<String>> parent2Childrean = new HashMap<String, Set<String>>();
		Collection<String> output = new ArrayList<String>();
		if (allMetadatas == null || targetMetadatas == null) {
			output.add(generateDefaultGroup());
			return output;
		}
		for (String metadata : allMetadatas) {
			ObjectNode node = JSONUtility.fromObjectString(metadata);
			String type = node.get(MessageFields.TYPE).asText();
			// pass by group metadata
			if (ProcessorsGroup.isGroupType(type) || DataSourceMetadataFactory.isDataSourceType(type)
					|| PublishProcessorFactory.isPublishProcessor(type)) {
				continue;
			} else {
				AbstractMetadata data = MetadataFactory.parseJsonMetadata(metadata);
				List<String> parents = data.getParentProcessors();
				if (parents != null && parents.size() >= 1) {
					for (String parent : parents) {
						if (parent2Childrean.get(parent) == null) {
							parent2Childrean.put(parent, new HashSet<String>());
						}
						parent2Childrean.get(parent).add(data.getProcessorid());
					}
				}
			}
		}

		for (String target : targetMetadatas) {
			ObjectNode node = JSONUtility.fromObjectString(target);
			resetIdField(node);
			String type = node.get(MessageFields.TYPE).asText();
			if (!ProcessorsGroup.isGroupType(type) && !DataSourceMetadataFactory.isDataSourceType(type)) {
				setSuccessors(node, parent2Childrean);
			}
			if (type.equals(MessageFields.GROUP_TYPE_SQL)) {
				String convertedSQL = convertSQL2NodeRead(node);
				output.add(convertedSQL);
			} else if (type.equals(MessageFields.GROUP_TYPE_TAB)) {
				if (node.get(idField).asText().equals(ProcessorsGroup.DEFAULT_GROUPTID)) {
					node.put(idField, 0);
				}
				output.add(node.toString());
			} else {
				output.add(node.toString());
			}
		}
		output = resetInputProcess(output);
		return output;
	}

	/**
	 * get data source for input
	 * 
	 * @param array
	 * @param tenantid
	 * @return
	 * @throws CPSException
	 */
	private Collection<String> resetInputProcess(Collection<String> outputs) throws CPSException {
		ObjectNode objectNode;
		Collection<String> retCollection = new ArrayList<String>();
		String type;
		for (String output : outputs) {
			objectNode = JSONUtility.fromObjectString(output);
			type = JSONUtility.GetString(MessageFields.TYPE, objectNode);
			ValidatorUtil.checkNotNull(MessageFields.TYPE, type);
			if (ProcessorFactory.isInput(type)) {
				objectNode.remove(MessageFields.SCHEMA);
				objectNode.remove(MessageFields.SCHEMAID);
				objectNode.remove(MessageFields.SCHEMA_FORMAT_TYPE);
				objectNode.remove(MessageFields.PARENTPROCESSORS);
			}
			retCollection.add(objectNode.toString());
		}
		return retCollection;
	}

	private String convertSQL2NodeRead(ObjectNode node) {
		node.put(MessageFields.TYPE, MessageFields.GROUP_TYPE_TAB);
		node.remove(MessageFields.PROCESSOR_GROUP_CONTENT);
		node.put("label", node.get(MessageFields.NAME).asText());
		node.remove(MessageFields.NAME);
		return node.toString();
	}

	private String generateDefaultGroup() {
		ObjectNode node = JSONUtility.newObjectNode();
		node.put(MessageFields.TYPE, MessageFields.GROUP_TYPE_TAB);
		String id = RandomUtil.getRandomId();
		node.put(idField, id);
		node.put("label", "Node-red default");
		return node.toString();
	}

	public ObjectNode genenrateDefaultGroup(String groupid) {
		ObjectNode node = JSONUtility.newObjectNode();
		node.put(MessageFields.TYPE, MessageFields.GROUP_TYPE_TAB);
		node.put(idField, groupid);
		node.put("label", "Node-red default");
		return node;
	}

}
